{
  "cells": [
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "_"
      },
      "source": [
        "# Haiku and `jax2tf`\n",
        "\n",
        "`jax2tf` is an advanced JAX feature supporting staging JAX programs out as TensorFlow graphs.\n",
        "\n",
        "This is a useful feature if you want to integrate with an existing TensorFlow codebase or tool. In this tutorial we will demonstrate defining a simple model in Haiku, converting it to TensorFlow as a `tf.Module` and then training it.\n",
        "\n",
        "We'll then save the model as a [TensorFlow SavedModel](https://www.tensorflow.org/guide/saved_model) so it can be used later in other TensorFlow programs."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 1,
      "metadata": {
        "id": "_"
      },
      "outputs": [],
      "source": [
        "!pip install dm-tree dm-sonnet tensorflow tensorflow_datasets ipywidgets matplotlib >/dev/null"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 2,
      "metadata": {
        "id": "_"
      },
      "outputs": [],
      "source": [
        "import haiku as hk\n",
        "import jax\n",
        "import jax.numpy as jnp\n",
        "from jax.experimental import jax2tf\n",
        "import sonnet as snt\n",
        "import tensorflow as tf\n",
        "import tree"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "_"
      },
      "source": [
        "## Define your model in JAX\n",
        "\n",
        "First things first, we need to define our model using Haiku and JAX. For MNIST we can use a trivial model like an MLP.\n",
        "\n",
        "We initialize the model using JAX and get initial parameter values. If you wanted you could additionally go on to train your model using JAX, but in this example we will do that in TensorFlow."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 3,
      "metadata": {
        "id": "_"
      },
      "outputs": [
        {
          "name": "stderr",
          "output_type": "stream",
          "text": [
            "WARNING:absl:No GPU/TPU found, falling back to CPU. (Set TF_CPP_MIN_LOG_LEVEL=0 and rerun for more info.)\n"
          ]
        }
      ],
      "source": [
        "def f(x):\n",
        "  net = hk.nets.MLP([300, 100, 10])\n",
        "  return net(x)\n",
        "\n",
        "f = hk.transform(f)\n",
        "\n",
        "rng = jax.random.PRNGKey(42)\n",
        "x = jnp.ones([1, 28 * 28 * 1])\n",
        "params = f.init(rng, x)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "_"
      },
      "source": [
        "## Convert to TensorFlow\n",
        "\n",
        "TensorFlow ships with a module abstraction that supports common tasks like collecting model parameters.\n",
        "\n",
        "Sonnet is a library of `tf.Module` subclasses including common NN layers, optimizers and some metrics. Sonnet is a sister library to Haiku developed by the same team.\n",
        "\n",
        "We will use Sonnet's module class for some nice `name_scope`-ing and later we will use the Adam optimizer implemented in Sonnet as well as some utility functions."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 4,
      "metadata": {
        "id": "_"
      },
      "outputs": [
        {
          "data": {
            "text/plain": [
              "['jax_module/mlp/_/linear_0/b:0',\n",
              " 'jax_module/mlp/_/linear_0/w:0',\n",
              " 'jax_module/mlp/_/linear_1/b:0',\n",
              " 'jax_module/mlp/_/linear_1/w:0',\n",
              " 'jax_module/mlp/_/linear_2/b:0',\n",
              " 'jax_module/mlp/_/linear_2/w:0']"
            ]
          },
          "execution_count": 1,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "def create_variable(path, value):\n",
        "  name = '/'.join(map(str, path)).replace('~', '_')\n",
        "  return tf.Variable(value, name=name)\n",
        "\n",
        "class JaxModule(snt.Module):\n",
        "  def __init__(self, params, apply_fn, name=None):\n",
        "    super().__init__(name=name)\n",
        "    self._params = tree.map_structure_with_path(create_variable, params)\n",
        "    self._apply = jax2tf.convert(lambda p, x: apply_fn(p, None, x))\n",
        "    self._apply = tf.autograph.experimental.do_not_convert(self._apply)\n",
        "\n",
        "  def __call__(self, inputs):\n",
        "    return self._apply(self._params, inputs)\n",
        "\n",
        "net = JaxModule(params, f.apply)\n",
        "[v.name for v in net.trainable_variables]"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "_"
      },
      "source": [
        "## Train using TensorFlow\n",
        "\n",
        "TensorFlow datasets is a great library with lots of common datasets that you might want to do research with. Here we will use it to load the MNIST handwritten digit dataset and define a simple pipeline that will randomly shuffle training images and normalize them into `[0, 1)`."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 5,
      "metadata": {
        "id": "_"
      },
      "outputs": [],
      "source": [
        "import tensorflow_datasets as tfds\n",
        "\n",
        "ds_train, ds_test = tfds.load('mnist', split=('train', 'test'),\n",
        "                              shuffle_files=True, as_supervised=True)\n",
        "\n",
        "def normalize_img(image, label):\n",
        "  \"\"\"Normalizes images: `uint8` -> `float32`.\"\"\"\n",
        "  image = tf.cast(image, tf.float32) / 255.\n",
        "  return image, label\n",
        "\n",
        "ds_train = ds_train.map(normalize_img, num_parallel_calls=tf.data.experimental.AUTOTUNE)\n",
        "ds_train = ds_train.cache()\n",
        "ds_train = ds_train.shuffle(60000)\n",
        "ds_train = ds_train.batch(100)\n",
        "ds_train = ds_train.repeat()\n",
        "ds_train = ds_train.prefetch(tf.data.experimental.AUTOTUNE)\n",
        "\n",
        "ds_test = ds_test.map(normalize_img, num_parallel_calls=tf.data.experimental.AUTOTUNE)\n",
        "ds_test = ds_test.batch(100)\n",
        "ds_test = ds_test.cache()\n",
        "ds_test = ds_test.prefetch(tf.data.experimental.AUTOTUNE)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "_"
      },
      "source": [
        "In order to train our model we need a training loop that updates model parameters based on gradients for some loss. For this example we will use the Adam optimizer from Sonnet and perform a gradient update to our parameters for each mini-batch."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 6,
      "metadata": {
        "id": "_"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Step 0: 2.309901475906372\n",
            "Step 1000: 0.23313118517398834\n",
            "Step 2000: 0.058662284165620804\n",
            "Step 3000: 0.060427404940128326\n",
            "Step 4000: 0.07748399674892426\n",
            "Step 5000: 0.07069656997919083\n",
            "Step 6000: 0.03870276361703873\n"
          ]
        }
      ],
      "source": [
        "net = JaxModule(params, f.apply)\n",
        "opt = snt.optimizers.Adam(1e-3)\n",
        "\n",
        "@tf.function(experimental_compile=True, autograph=False)\n",
        "def train_step(images, labels):\n",
        "  \"\"\"Performs one optimizer step on a single mini-batch.\"\"\"\n",
        "  with tf.GradientTape() as tape:\n",
        "    images = snt.flatten(images)\n",
        "    logits = net(images)\n",
        "    loss = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=logits,\n",
        "                                                          labels=labels)\n",
        "    loss = tf.reduce_mean(loss)\n",
        "    params = tape.watched_variables()\n",
        "    loss += 1e-4 * sum(map(tf.nn.l2_loss, params))\n",
        "\n",
        "  grads = tape.gradient(loss, params)\n",
        "  opt.apply(grads, params)\n",
        "  return loss\n",
        "\n",
        "for step, (images, labels) in enumerate(ds_train.take(6001)):\n",
        "  loss = train_step(images, labels)\n",
        "  if step % 1000 == 0:\n",
        "    print(f\"Step {step}: {loss.numpy()}\")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "_"
      },
      "source": [
        "To evaluate how our newly trained model performs we can use top-1 accuracy on our test set."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 7,
      "metadata": {
        "id": "_"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Got 9805/10000 (98.05%) correct\n"
          ]
        }
      ],
      "source": [
        "def accuracy(model):\n",
        "  total = 0\n",
        "  correct = 0\n",
        "  for images, labels in ds_test:\n",
        "    predictions = tf.argmax(model(snt.flatten(images)), axis=1)\n",
        "    correct += tf.math.count_nonzero(tf.equal(predictions, labels))\n",
        "    total += images.shape[0]\n",
        "\n",
        "  print(\"Got %d/%d (%.02f%%) correct\" % (correct, total, correct / total * 100.))\n",
        "\n",
        "accuracy(net)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "_"
      },
      "source": [
        "It is useful to visualize predictions the model is making against the input we are providing. This can be particularly useful where the model mispredicts the label, you can see that in some cases the handwriting is a bit dubious!"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 8,
      "metadata": {
        "id": "_"
      },
      "outputs": [],
      "source": [
        "import matplotlib.pyplot as plt\n",
        "\n",
        "def sample(correct, rows, cols):\n",
        "  \"\"\"Utility function to show a sample of images.\"\"\"\n",
        "  n = 0\n",
        "\n",
        "  f, ax = plt.subplots(rows, cols)\n",
        "  if rows > 1:\n",
        "    ax = tf.nest.flatten([tuple(ax[i]) for i in range(rows)])\n",
        "  f.set_figwidth(14)\n",
        "  f.set_figheight(4 * rows)\n",
        "\n",
        "  for images, labels in ds_test:\n",
        "    predictions = tf.argmax(net(snt.flatten(images)), axis=1)\n",
        "    eq = tf.equal(predictions, labels)\n",
        "    for i, x in enumerate(eq):\n",
        "      if x.numpy() == correct:\n",
        "        label = labels[i]\n",
        "        prediction = predictions[i]\n",
        "        image = tf.squeeze(images[i])\n",
        "\n",
        "        ax[n].imshow(image)\n",
        "        ax[n].set_title(\"Prediction:{}\\nActual:{}\".format(prediction, label))\n",
        "\n",
        "        n += 1\n",
        "        if n == (rows * cols):\n",
        "          break\n",
        "\n",
        "    if n == (rows * cols):\n",
        "      break"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 9,
      "metadata": {
        "id": "_"
      },
      "outputs": [
        {
          "data": {
            "image/png": "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\n",
            "text/plain": [
              "<Figure size 1008x288 with 5 Axes>"
            ]
          },
          "metadata": {
            "needs_background": "light"
          },
          "output_type": "display_data"
        }
      ],
      "source": [
        "sample(correct=True, rows=1, cols=5)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 10,
      "metadata": {
        "id": "_"
      },
      "outputs": [
        {
          "data": {
            "image/png": "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\n",
            "text/plain": [
              "<Figure size 1008x576 with 10 Axes>"
            ]
          },
          "metadata": {
            "needs_background": "light"
          },
          "output_type": "display_data"
        }
      ],
      "source": [
        "sample(correct=False, rows=2, cols=5)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "_"
      },
      "source": [
        "## Save to disk as a TensorFlow SavedModel\n",
        "\n",
        "It is very common to take a model trained using TensorFlow and save it to disk as a \"saved model\". This is a language independent format that allows you to load your model code using Python, C++ or other languages supported by TensorFlow.\n",
        "\n",
        "### Saving\n",
        "\n",
        "In order to save our model to disk, we need to define what the functions are that we want to save, and provide references to any state we want to save:"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 11,
      "metadata": {
        "id": "_"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "INFO:tensorflow:Assets written to: /tmp/example_saved_model/assets\n"
          ]
        },
        {
          "name": "stderr",
          "output_type": "stream",
          "text": [
            "INFO:tensorflow:Assets written to: /tmp/example_saved_model/assets\n"
          ]
        }
      ],
      "source": [
        "@tf.function(autograph=False, input_signature=[tf.TensorSpec([100, 28 * 28])])\n",
        "def forward(x):\n",
        "  return net(x)\n",
        "\n",
        "to_save = tf.Module()\n",
        "to_save.forward = forward\n",
        "to_save.params = list(net.variables)\n",
        "tf.saved_model.save(to_save, \"/tmp/example_saved_model\")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "_"
      },
      "source": [
        "### Loading\n",
        "\n",
        "Loading a saved model is trivial, and you can see that this looks a lot like the model we saved:"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 12,
      "metadata": {
        "id": "_"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "WARNING:tensorflow:Importing a function (__inference_forward_26770) with ops with custom gradients. Will likely fail if a gradient is requested.\n"
          ]
        },
        {
          "name": "stderr",
          "output_type": "stream",
          "text": [
            "WARNING:tensorflow:Importing a function (__inference_forward_26770) with ops with custom gradients. Will likely fail if a gradient is requested.\n"
          ]
        },
        {
          "data": {
            "text/plain": [
              "['jax_module/mlp/_/linear_0/b:0',\n",
              " 'jax_module/mlp/_/linear_0/w:0',\n",
              " 'jax_module/mlp/_/linear_1/b:0',\n",
              " 'jax_module/mlp/_/linear_1/w:0',\n",
              " 'jax_module/mlp/_/linear_2/b:0',\n",
              " 'jax_module/mlp/_/linear_2/w:0']"
            ]
          },
          "execution_count": 1,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "loaded = tf.saved_model.load(\"/tmp/example_saved_model\")\n",
        "preds = loaded.forward(tf.ones([100, 28 * 28]))\n",
        "assert preds.shape == [100, 10]\n",
        "assert len(loaded.params) == 6\n",
        "\n",
        "[v.name for v in loaded.params]"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "_"
      },
      "source": [
        "Thankfully the restored model performs just as well as the model that we saved:"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 13,
      "metadata": {
        "id": "_"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Got 9805/10000 (98.05%) correct\n"
          ]
        }
      ],
      "source": [
        "accuracy(loaded.forward)"
      ]
    }
  ],
  "metadata": {
    "colab": {}
  },
  "nbformat": 4,
  "nbformat_minor": 0
}
